Оглавление

  • Рынок заведений общественного питания Москвы
    • Загрузим данные и изучим общую информацию
      • Считаем данные из csv-файла в датафрейм и сохраняем в переменную data
      • Изучим общую информацию о датасете
    • Выполним предобработку данных
      • Проверим на наличие дубликатов
      • Изучим пропуски в данных
      • Создадим столбец street с названиями улиц из столбца с адресом
      • Создадим столбец is_24_7 с обозначением, что заведение работает ежедневно и круглосуточно
      • Создадим столбец distr_short с аббревиатурой для каждого района
    • Анализ данных
      • Проанализируем распределение заведений по категориям
      • Исследуем количество посадочных мест в заведениях по категориям
      • Рассмотрим и изобразим соотношение сетевых и несетевых заведений в датасете
      • Выясним, какие категории заведений чаще являются сетевыми
      • Сгруппируем данные по названиям заведений и найдем топ-15 популярных сетей в Москве (по количеству заведений)
      • Проанализируем количество заведений в разных районах Москвы
      • Проанализируем средние (медианные) рейтинги в разных категориях заведений
      • Построим фоновую картограмму со средним (медианным) рейтингом заведений каждого района, отобразим все заведения датасета на карте
      • Проанализируем улицы Москвы по количеству заведений
        • Найдем топ-15 улиц по количеству заведений
        • Построим график распределения количества заведений и их категорий по топ-15 улицам
        • Найдем улицы, на которых находится только один объект общепита
      • Проанализируем примерную стоимость заказа в рублях и её изменение в зависимости от расположения завдения
      • Общий вывод по разделу
    • Детализируем исследование: открытие кофейни
      • Особенности расположения
        • Выделим улицы СЗАО, где отсутствуют другие кофейни в новый датафрейм data_streets_0
        • Выделим улицы СЗАО с большой концентрацией несетевых кофеен (2 и более) в новый датафрейм data_streets_1
      • Время работы
        • Проверим data_streets_1 на наличие круглосуточных кофеен
      • Рейтинги
        • Проанализируем средние рейтинги в data_streets_1 (улицы с большой концентрацией несетевых кофеен)
        • Проанализируем средние рейтинги в data_streets_0 (улицы, где отсутствуют кофейни)
      • Цены на капучино
      • Общий вывод по разделу

Рынок заведений общественного питания Москвы¶

Описание проекта

Инвесторы из фонда «Shut Up and Take My Money» решили попробовать себя в новой области и открыть заведение общественного питания в Москве. Цель проекта: подготовить исследование рынка Москвы, найти интересные особенности и презентовать полученные результаты, которые в будущем помогут в выборе подходящего инвесторам места.

Описание данных

Нам доступен датасет с заведениями общественного питания Москвы, составленный на основе данных сервисов Яндекс Карты и Яндекс Бизнес на лето 2022 года. Информация, размещённая в сервисе Яндекс Бизнес, могла быть добавлена пользователями или найдена в общедоступных источниках. Она носит исключительно справочный характер.

Файл moscow_places.csv:

  • name — название заведения;
  • address — адрес заведения;
  • category — категория заведения, например «кафе», «пиццерия» или «кофейня»;
  • hours — информация о днях и часах работы;
  • lat — широта географической точки, в которой находится заведение;
  • lng — долгота географической точки, в которой находится заведение;
  • rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);
  • price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;
  • avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:
    • «Средний счёт: 1000–1500 ₽»;
    • «Цена чашки капучино: 130–220 ₽»;
    • «Цена бокала пива: 400–600 ₽». и так далее;
  • middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»:
    • Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
    • Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
    • Если значения нет или оно не начинается с подстроки «Средний счёт», то в столбец ничего не войдёт.
  • middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»:
    • Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
    • Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
    • Если значения нет или оно не начинается с подстроки «Цена одной чашки капучино», то в столбец ничего не войдёт.
  • chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки):
    • 0 — заведение не является сетевым
    • 1 — заведение является сетевым
  • district — административный район, в котором находится заведение, например Центральный административный округ;
  • seats — количество посадочных мест.

Загрузим данные и изучим общую информацию¶

In [1]:
# импортируем библиотеки
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import re
import plotly.express as px
from plotly import graph_objects as go
import json
from folium import Map, Choropleth
from folium import GeoJsonTooltip
import os
from folium import Map, Marker
from folium.plugins import MarkerCluster
In [2]:
# убираем предупреждения
pd.options.mode.chained_assignment = None  # default='warn'
from matplotlib.axes._axes import _log as matplotlib_axes_logger
matplotlib_axes_logger.setLevel('ERROR')

# установим максимальное количество отображающихся столбцов
pd.set_option('display.max_columns', None)

# установим максимальную ширину отображающихся столбцов
pd.set_option('max_colwidth', 120)

# установим максимальное количество отображающихся элементов серии
np.set_printoptions(threshold=50) # 50 заменить на np.inf, если хотим показать все элементы

Считаем данные из csv-файла в датафрейм и сохраняем в переменную data¶

In [3]:
# считаем данные
try:
    data = pd.read_csv('/datasets/moscow_places.csv')
except:
    data = pd.read_csv('moscow_places.csv')
data.head(5)
Out[3]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
0 WoWфли кафе Москва, улица Дыбенко, 7/1 Северный административный округ ежедневно, 10:00–22:00 55.878494 37.478860 5.0 NaN NaN NaN NaN 0 NaN
1 Четыре комнаты ресторан Москва, улица Дыбенко, 36, корп. 1 Северный административный округ ежедневно, 10:00–22:00 55.875801 37.484479 4.5 выше среднего Средний счёт:1500–1600 ₽ 1550.0 NaN 0 4.0
2 Хазри кафе Москва, Клязьминская улица, 15 Северный административный округ пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00–02:00 55.889146 37.525901 4.6 средние Средний счёт:от 1000 ₽ 1000.0 NaN 0 45.0
3 Dormouse Coffee Shop кофейня Москва, улица Маршала Федоренко, 12 Северный административный округ ежедневно, 09:00–22:00 55.881608 37.488860 5.0 NaN Цена чашки капучино:155–185 ₽ NaN 170.0 0 NaN
4 Иль Марко пиццерия Москва, Правобережная улица, 1Б Северный административный округ ежедневно, 10:00–22:00 55.881166 37.449357 5.0 средние Средний счёт:400–600 ₽ 500.0 NaN 1 148.0

Изучим общую информацию о датасете¶

In [4]:
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8406 entries, 0 to 8405
Data columns (total 14 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   name               8406 non-null   object 
 1   category           8406 non-null   object 
 2   address            8406 non-null   object 
 3   district           8406 non-null   object 
 4   hours              7870 non-null   object 
 5   lat                8406 non-null   float64
 6   lng                8406 non-null   float64
 7   rating             8406 non-null   float64
 8   price              3315 non-null   object 
 9   avg_bill           3816 non-null   object 
 10  middle_avg_bill    3149 non-null   float64
 11  middle_coffee_cup  535 non-null    float64
 12  chain              8406 non-null   int64  
 13  seats              4795 non-null   float64
dtypes: float64(6), int64(1), object(7)
memory usage: 919.5+ KB

Вывод

  • в датасете представлено 8406 заведений
  • в датасете имеютсмя столбцы как в числовом формате float64 и int, так и в текстовом формате object
  • столбцы price и average_bill хранят категориальные данные, поэтому данные в этих столбцах представлены в текстовом формате object.

Выполним предобработку данных¶

Проверим на наличие дубликатов¶

Проверим наличие дублирующихся строк.

In [5]:
data[data.duplicated()]
Out[5]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats

Строки-дубликаты (явные/полные дубликаты) отсутствуют.

Также проверим наличие неявных дубликатов в столбце name для сетевых заведений (для несетевых заведений сложно угадать, это неявный дубликат или совершенно другое заведение).

In [6]:
data.query('chain==1')['name'].sort_values().unique()
Out[6]:
array(['1-я Креветочная', '10 Идеальных Пицц', '18 Грамм', ...,
       'Яндекс Лавка', 'Яндекс.Лавка', 'Японская кухня'], dtype=object)

В стобце name встречаются разные названия одной и той же сети:

  • 'Яндекс Лавка', 'Яндекс.Лавка'
  • 'Чайхона № 1', 'Чайхона №1'
  • 'Чайхана Халаль', 'Чайхана Халяль', 'Чайхана халяль'
  • "Домино'с Пицца", "Домино'с пицца", 'Доминос пицца'

Переименуем их на уникальное название.

In [7]:
data['name'] = data['name'].replace(['Яндекс Лавка', 'Яндекс.Лавка'], 'Яндекс.Лавка')
data['name'] = data['name'].replace(['Чайхона № 1', 'Чайхона №1'], 'Чайхона №1')
data['name'] = data['name'].replace(['Чайхана Халаль', 'Чайхана Халяль', 'Чайхана халяль'], 'Чайхана Халяль')
data['name'] = data['name'].replace(["Домино'с Пицца", "Домино'с пицца", "Доминос пицца"], "Домино'с Пицца")

Изучим пропуски в данных¶

In [8]:
for column in data.columns:
    if data[column].isna().sum()>0:
        print("Название столбца:", column)
        print("Количество пропусков:", data[column].isna().sum())
        print('----')
print("В остальных столбцах пропуски отсутствуют")
Название столбца: hours
Количество пропусков: 536
----
Название столбца: price
Количество пропусков: 5091
----
Название столбца: avg_bill
Количество пропусков: 4590
----
Название столбца: middle_avg_bill
Количество пропусков: 5257
----
Название столбца: middle_coffee_cup
Количество пропусков: 7871
----
Название столбца: seats
Количество пропусков: 3611
----
В остальных столбцах пропуски отсутствуют

Пропуски в столбцах hours, price, avg_bill и seats скорее всего обозначает отсутствие данных о времени работы заведения, поэтому заполним пропуски значением no_info.

In [9]:
# заполняем пропуски и nan в столбцах hours, price, avg_bill и seats значением 'no_info'
data[['hours', 'price', 'avg_bill', 'seats']] = data[['hours', 'price', 'avg_bill', 'seats']].fillna('no_info')

Пропуски в столбце middle_avg_bill могут означать, что столбец avg_bill не начинаетя с подстроки «Средний счёт», а пропуски в столбце middle_coffee_cup - что столбец avg_bill не начинаетя с подстроки «Цена одной чашки капучино». Если эта догадка верная, заменим пропуски значением N/A - not applicable.

In [10]:
# проверим условие для столбца middle_avg_bill
if data[~data['avg_bill'].str.startswith("Средний счёт")]['middle_avg_bill'].isna().sum() == data['middle_avg_bill'].isna().sum():
    print('Пропуски в столбце middle_avg_bill встречаются только в тех случаях, когда значения из столбца avg_bill не начинаются')
    print('с подстроки «Средний счёт». Пропуски заменили значением N\A.')
    
    # заменим пропуски на 'N\A'
    data['middle_avg_bill'] = data['middle_avg_bill'].fillna('N/A')
else:
    print('Догадка неверная.')
print('----')

# проверим условие для столбца middle_coffee_cup
if data[~data['avg_bill'].str.startswith("Цена одной чашки капучино")]['middle_coffee_cup'].isna().sum() == data['middle_coffee_cup'].isna().sum():
    print('Пропуски в столбце middle_coffee_cup встречаются только в тех случаях, когда значения из столбца avg_bill не начинаются')
    print('с подстроки «Цена одной чашки капучино». Пропуски заменили значением N\A.')
    
    # заменим пропуски на 'N\A'
    data['middle_coffee_cup'] = data['middle_coffee_cup'].fillna('N/A')
else:
    print('Догадка неверная.')
Пропуски в столбце middle_avg_bill встречаются только в тех случаях, когда значения из столбца avg_bill не начинаются
с подстроки «Средний счёт». Пропуски заменили значением N\A.
----
Пропуски в столбце middle_coffee_cup встречаются только в тех случаях, когда значения из столбца avg_bill не начинаются
с подстроки «Цена одной чашки капучино». Пропуски заменили значением N\A.

Создадим столбец street с названиями улиц из столбца с адресом¶

In [11]:
data['street'] = data['address'].apply(lambda x: x.split(',')[1].strip())
data[['street']]
Out[11]:
street
0 улица Дыбенко
1 улица Дыбенко
2 Клязьминская улица
3 улица Маршала Федоренко
4 Правобережная улица
... ...
8401 Профсоюзная улица
8402 Пролетарский проспект
8403 Люблинская улица
8404 Люблинская улица
8405 Россошанский проезд

8406 rows × 1 columns

Создадим столбец is_24_7 с обозначением, что заведение работает ежедневно и круглосуточно¶

In [12]:
data['is_24_7'] = data['hours'] == 'ежедневно, круглосуточно'
data[['is_24_7']]
Out[12]:
is_24_7
0 False
1 False
2 False
3 False
4 False
... ...
8401 False
8402 False
8403 True
8404 True
8405 True

8406 rows × 1 columns

Создадим столбец distr_short с аббревиатурой для каждого района¶

In [13]:
# Функция для преобразования значения в аббревиатуру
def get_initials(text):
    words = text.replace('-', ' ').split()
    initials = ""
    for word in words:
        initials += word[0].upper()
    return initials

# Создание столбца с аббревиатурами
data['distr_short'] = data['district'].apply(get_initials)
data['distr_short'].unique()
Out[13]:
array(['САО', 'СВАО', 'СЗАО', 'ЗАО', 'ЦАО', 'ВАО', 'ЮВАО', 'ЮАО', 'ЮЗАО'],
      dtype=object)

Вывод

Итоговый датасет после предобработки данных и с новыми столбцами выглядит следующим образом.

In [14]:
data
Out[14]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats street is_24_7 distr_short
0 WoWфли кафе Москва, улица Дыбенко, 7/1 Северный административный округ ежедневно, 10:00–22:00 55.878494 37.478860 5.0 no_info no_info N/A N/A 0 no_info улица Дыбенко False САО
1 Четыре комнаты ресторан Москва, улица Дыбенко, 36, корп. 1 Северный административный округ ежедневно, 10:00–22:00 55.875801 37.484479 4.5 выше среднего Средний счёт:1500–1600 ₽ 1550.0 N/A 0 4.0 улица Дыбенко False САО
2 Хазри кафе Москва, Клязьминская улица, 15 Северный административный округ пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00–02:00 55.889146 37.525901 4.6 средние Средний счёт:от 1000 ₽ 1000.0 N/A 0 45.0 Клязьминская улица False САО
3 Dormouse Coffee Shop кофейня Москва, улица Маршала Федоренко, 12 Северный административный округ ежедневно, 09:00–22:00 55.881608 37.488860 5.0 no_info Цена чашки капучино:155–185 ₽ N/A 170.0 0 no_info улица Маршала Федоренко False САО
4 Иль Марко пиццерия Москва, Правобережная улица, 1Б Северный административный округ ежедневно, 10:00–22:00 55.881166 37.449357 5.0 средние Средний счёт:400–600 ₽ 500.0 N/A 1 148.0 Правобережная улица False САО
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
8401 Суши Мания кафе Москва, Профсоюзная улица, 56 Юго-Западный административный округ ежедневно, 09:00–02:00 55.670021 37.552480 4.4 no_info no_info N/A N/A 0 86.0 Профсоюзная улица False ЮЗАО
8402 Миславнес кафе Москва, Пролетарский проспект, 19, корп. 1 Южный административный округ ежедневно, 08:00–22:00 55.640875 37.656553 4.8 no_info no_info N/A N/A 0 150.0 Пролетарский проспект False ЮАО
8403 Самовар кафе Москва, Люблинская улица, 112А, стр. 1 Юго-Восточный административный округ ежедневно, круглосуточно 55.648859 37.743219 3.9 no_info Средний счёт:от 150 ₽ 150.0 N/A 0 150.0 Люблинская улица True ЮВАО
8404 Чайхана Sabr кафе Москва, Люблинская улица, 112А, стр. 1 Юго-Восточный административный округ ежедневно, круглосуточно 55.648849 37.743222 4.2 no_info no_info N/A N/A 1 150.0 Люблинская улица True ЮВАО
8405 Kebab Time кафе Москва, Россошанский проезд, 6 Южный административный округ ежедневно, круглосуточно 55.598229 37.604702 3.9 no_info no_info N/A N/A 0 12.0 Россошанский проезд True ЮАО

8406 rows × 17 columns

Анализ данных¶

Проанализируем распределение заведений по категориям¶

In [15]:
# Определяем порядок сортировки
sorted_data1 = data['category'].value_counts().sort_values(ascending=False).reset_index()
sorted_data1.columns = ['category', 'count']

# Задаем размер графика
plt.figure(figsize=(8, 6))

# Создаем график
graph1 = sns.barplot(x='category', y='count', data=sorted_data1)

# Поворачиваем подписи на 45 градусов
labels1 = graph1.get_xticklabels()
graph1.set_xticklabels(labels1, rotation=45)

# Добавляем заголовок и подписи осей
plt.title('Распределение заведений по категориям')
plt.xlabel('Категории заведений')
plt.ylabel('Количество')

# Отображаем график
plt.show()

Выводы

  • больше всего объектов общественного питания представлены в категории кафе, далее - рестораны
  • меньше всего встречается столовые и булочные.

Исследуем количество посадочных мест в заведениях по категориям¶

In [16]:
# Фильтруем только те заведения, где имеется информация о количестве стульев
sorted_data2 = data.query('seats != "no_info"')

# Сохранияем информацию о стульях в формате int
sorted_data2['seats'] = sorted_data2['seats'].astype(int)

# Определяем порядок сортировки
sorted_data2 = sorted_data2.groupby('category').agg({'seats': 'median'}).sort_values('seats', ascending=False).reset_index()

# Задаем размер графика
plt.figure(figsize=(8, 6))

# Создаем график
graph2 = sns.barplot(x='category', y='seats', data=sorted_data2)

# Поворачиваем подписи на 45 градусов
labels2 = graph2.get_xticklabels()
graph2.set_xticklabels(labels2, rotation=45)

# Добавляем значения над каждым столбцом
for index, row in sorted_data2.iterrows():
    graph2.annotate('{:.0f}'.format(row['seats']), xy=(index, row['seats']), ha='center', va='bottom')

# Добавляем заголовок и подписи осей
plt.title('Медианное количество посадочных мест в заведениях')
plt.xlabel('Категории заведений')
plt.ylabel('Медианное количество мест')

# Отображаем график
plt.show()

Выводы

  • самыми крупными заведениями по медианному количеству посадодчных мест являются рестораны, далее - бары/пабы и кофейни
  • меньше всего посадочных мест можно встретить в булочных, в среднем - 50 стульев (медианное значение)

Рассмотрим и изобразим соотношение сетевых и несетевых заведений в датасете¶

In [17]:
# готовим данные для графика
sorted_data3 = data['chain'].value_counts().sort_values(ascending=False).reset_index()
sorted_data3.columns = ['type', 'count']
sorted_data3
Out[17]:
type count
0 0 5201
1 1 3205
In [18]:
# готовим данные для графика
sorted_data3['type2'] = sorted_data3['type']
sorted_data3['type2'] = sorted_data3['type2'].replace([0, 1], ['несетевое', 'сетевое'])


# строим круговую диаграмму
fig3 = go.Figure(data=[go.Pie(labels=sorted_data3['type2'], # указываем значения, которые появятся на метках сегментов
                             values=sorted_data3['count'], # указываем данные, которые отобразятся на графике
                             pull = [0.1, 0])]) # добавляем аргумент, который выделит тип-лидер на графике
fig3.update_layout(title='Соотношение сетевых и несетевых заведений', # указываем заголовок графика
                  width=700, # указываем размеры графика
                  height=500,
                  annotations=[dict(x=1.12, # вручную настраиваем аннотацию легенды
                                    y=1.05,
                                    text='Количество заведений',
                                    showarrow=False)])
fig3.show() # выводим график

Вывод

  • несетевых заведений больше, они составляют 61.9% всех заведений.

Выясним, какие категории заведений чаще являются сетевыми¶

In [19]:
# Подготовим данные для графика
sorted_data4 = data.groupby(['category', 'chain']).agg({'name': 'count'}).reset_index()
sorted_data4['chain'] = sorted_data4['chain'].replace([0, 1], ['несетевое', 'сетевое'])
sorted_data4.rename(columns={'name': 'count'}, inplace=True)
sorted_data4 = sorted_data4.sort_values('count', ascending=False).reset_index()

# Задаем размер графика
plt.figure(figsize=(10, 7))

# Создаем график
graph4 = sns.barplot(x='category', y='count', hue='chain', data=sorted_data4)

# Поворачиваем подписи на 45 градусов
labels4 = graph4.get_xticklabels()
graph4.set_xticklabels(labels4, rotation=45)

# Убираем надпись 'chain' из легенды
graph4.legend(title=None)

# Добавляем заголовок и подписи осей
plt.title('Категории сетевых и несетевых заведений')
plt.xlabel('Категории')
plt.ylabel('Количество заведений')

# Отображаем график
plt.show()

Вывод

Сетевыми заведениями чаще являются кофейни, пицерии и булочные. У остальных категорий заведений чаще встречаются несетевые типы.

Сгруппируем данные по названиям заведений и найдем топ-15 популярных сетей в Москве (по количеству заведений)¶

In [20]:
# Подготовим данные для графика
sorted_data5 = data.query('chain==1')
sorted_data5 = sorted_data5.groupby(['name', 'category']).agg({'address': 'count'}).reset_index()
sorted_data5.rename(columns={'address': 'count'}, inplace=True)
sorted_data5 = sorted_data5.sort_values('count', ascending=False).reset_index().drop('index', axis=1)
sorted_data5 = sorted_data5.head(15)
sorted_data5
Out[20]:
name category count
0 Шоколадница кофейня 119
1 Домино'с Пицца пиццерия 76
2 Додо Пицца пиццерия 74
3 Яндекс.Лавка ресторан 72
4 One Price Coffee кофейня 71
5 Cofix кофейня 65
6 Prime ресторан 49
7 КОФЕПОРТ кофейня 42
8 Кулинарная лавка братьев Караваевых кафе 39
9 Теремок ресторан 36
10 CofeFest кофейня 31
11 Чайхана кафе 26
12 Буханка булочная 25
13 Drive Café кафе 24
14 Кофемания кофейня 22
In [21]:
# строим столбчатую диаграмму 
fig5 = px.bar(sorted_data5.sort_values(by='count', ascending=True), # загружаем данные и заново их сортируем
             x='count', # указываем столбец с данными для оси X
             y='name', # указываем столбец с данными для оси Y
             text='count', # добавляем аргумент, который отобразит текст с информацией о количестве
              hover_data=['category'] # добавляем аргумент, который отобразит категорию при наведении курсора
            )

# оформляем график
fig5.update_layout(title='ТОП-15 популярных сетей по количеству заведений',
                   xaxis_title='Количество заведений',
                   yaxis_title='Название сети')
fig5.show() # выводим график
In [22]:
print('ТОП-15 популярных сетей по количеству заведений относятся к следующим категориям:', sorted_data5['category'].unique())
ТОП-15 популярных сетей по количеству заведений относятся к следующим категориям: ['кофейня' 'пиццерия' 'ресторан' 'кафе' 'булочная']

Выводы

  • самой популярной сетью в Москве является "Шоколадница" (119 заведений)
  • следующими по популярности являются сети пиццерий "Домино'c Пицца" и "Додо Пицца" (76 и 74 заведений, соответственно)
  • обобщенно можно сказать, что самыми популярными заведениями являются кофейни, пиццерии и другие заведения с узнаваемым брендом
  • сети, которые вошли в ТОП-15, имеют своих постоянных клиентов и пользуются популярностью и в других регионах.

Проанализируем количество заведений в разных районах Москвы¶

In [23]:
# Подготовим данные для графика
sorted_data6 = data.groupby(['distr_short', 'category']).agg({'address': 'count'}).reset_index()
sorted_data6.rename(columns={'address': 'count'}, inplace=True)
sorted_data6 = sorted_data6.sort_values('count', ascending=False).reset_index().drop('index', axis=1)
sorted_data6_distr = sorted_data6.groupby(['distr_short']).agg({'count': 'sum'}).reset_index()
sorted_data6 = pd.merge(sorted_data6, sorted_data6_distr, on='distr_short')
sorted_data6.rename(columns={'count_x': 'count_cat', 'count_y': 'count_distr'}, inplace=True)
In [24]:
# Отсортируем данные
sorted_data6 = sorted_data6.sort_values(by=['count_distr', 'count_cat'], ascending=False)

# Создание столбчатой диаграммы
fig6 = px.bar(sorted_data6, x='distr_short', y='count_cat', color='category', labels={
    'category': 'Категория заведения',
    'distr_short': 'Административный район',
    'count_cat': 'Количество заведений'
})

# Настройка внешнего вида диаграммы
fig6.update_layout(title='Количество заведений по районам и категориям',
                  xaxis_title='Административный район',
                  yaxis_title='Количество заведений')

# Установка размера графика
fig6.update_layout(width=950, height=600)

fig6.show()
In [25]:
print('В датасете присутствуют', data['district'].nunique(), 'административных районов:', data['district'].unique())
В датасете присутствуют 9 административных районов: ['Северный административный округ'
 'Северо-Восточный административный округ'
 'Северо-Западный административный округ'
 'Западный административный округ' 'Центральный административный округ'
 'Восточный административный округ' 'Юго-Восточный административный округ'
 'Южный административный округ' 'Юго-Западный административный округ']

Выводы

  • больше всего заведений представлено в ЦАО Москвы
  • в ЦАО самой распространенной категорией являются рестораны, а в остальных районах - кафе.

Проанализируем средние (медианные) рейтинги в разных категориях заведений¶

In [26]:
# Определяем порядок сортировки
sorted_data7 = data.groupby('category').agg({'rating': 'median'}).sort_values('rating', ascending=False).reset_index()

# Задаем размер графика
plt.figure(figsize=(8, 6))

# Создаем график
graph7 = sns.barplot(x='category', y='rating', data=sorted_data7)

# Поворачиваем подписи на 45 градусов
labels7 = graph7.get_xticklabels()
graph7.set_xticklabels(labels7, rotation=45)

# Добавляем значения над каждым столбцом
for index, row in sorted_data7.iterrows():
    graph7.annotate('{:.1f}'.format(row['rating']), xy=(index, row['rating']), ha='center', va='bottom')

# ограничиваем ось Y для наглядности
plt.ylim(3.8, 4.5)
    
# Добавляем заголовок и подписи осей
plt.title('Медианные рейтинги заведений')
plt.xlabel('Категории заведений')
plt.ylabel('Медианный рейтинг')

# Отображаем график
plt.show()
In [27]:
# Задаем размер графика
plt.figure(figsize=(9, 8))

# применяем стиль darkgrid из библиотеки seaborn
sns.set_style('darkgrid')

# строим график boxplot средствами seaborn
sns.boxplot(x='rating', y='category', data=data)

# указываем заголовок графика и подписи осей средствами matplotlib
plt.title('Распределение средней оценки в зависимости категории заведений')
plt.xlabel('Средняя оценка')
plt.ylabel('Категории заведений')

# отображаем график на экране
plt.show()

Выводы

  • бары/пабы обычно получают самую высокую среднюю оценку - 4.4
  • в целом можно утверждать, что усредненные рейтинги в разных типах общепита не сильно отличаются и находятся в пределах от 4.2 до 4.4

Построим фоновую картограмму со средним (медианным) рейтингом заведений каждого района, отобразим все заведения датасета на карте¶

В датасете представлено 9 округов.

Для каждого округа посчитаем медианный рейтинг торговых центров, которые находятся на его территории.

In [28]:
rating_data = data.groupby('district', as_index=False)['rating'].agg('median').sort_values(by='rating', ascending=False)
rating_data
Out[28]:
district rating
5 Центральный административный округ 4.4
0 Восточный административный округ 4.3
1 Западный административный округ 4.3
2 Северный административный округ 4.3
4 Северо-Западный административный округ 4.3
7 Юго-Западный административный округ 4.3
8 Южный административный округ 4.3
3 Северо-Восточный административный округ 4.2
6 Юго-Восточный административный округ 4.2
In [29]:
# загружаем JSON-файл с границами округов Москвы

pth1 = '/datasets/admin_level_geomap.geojson'
pth2 = 'admin_level_geomap.geojson'

if os.path.exists(pth1):
    state_geo = pth1
elif os.path.exists(pth2):
    state_geo = pth2
else:
    print('Something is wrong')
In [30]:
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423

# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10)

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=rating_data,
    columns=['district', 'rating'],
    key_on='feature.name',
    fill_color='YlGn',
    fill_opacity=0.8,
    legend_name='Медианный рейтинг заведений по районам',
).add_to(m)

# создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m)

# пишем функцию, которая принимает строку датафрейма,
# создаёт маркер в текущей точке и добавляет его в кластер marker_cluster
def create_clusters(row):
    Marker(
        [row['lat'], row['lng']],
        popup=f"{row['name']} {row['rating']}",
    ).add_to(marker_cluster)

# применяем функцию create_clusters() к каждой строке датафрейма
data.apply(create_clusters, axis=1)

# выводим карту
m
Out[30]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Проанализируем улицы Москвы по количеству заведений¶

Найдем топ-15 улиц по количеству заведений¶

In [31]:
sorted_data8 = data.groupby(['street', 'category']).agg({'address': 'count'}).reset_index()
sorted_data8.rename(columns={'address': 'count'}, inplace=True)
sorted_data8 = sorted_data8.sort_values('count', ascending=False).reset_index().drop('index', axis=1)
top_15_str = sorted_data8.groupby(['street']).agg({'count': 'sum'}).sort_values('count', ascending=False).reset_index().head(15)
top_15_str
Out[31]:
street count
0 проспект Мира 184
1 Профсоюзная улица 122
2 проспект Вернадского 108
3 Ленинский проспект 107
4 Ленинградский проспект 95
5 Дмитровское шоссе 88
6 Каширское шоссе 77
7 Варшавское шоссе 76
8 Ленинградское шоссе 70
9 МКАД 65
10 Люблинская улица 60
11 улица Вавилова 55
12 Кутузовский проспект 54
13 улица Миклухо-Маклая 49
14 Пятницкая улица 48

Построим график распределения количества заведений и их категорий по топ-15 улицам¶

In [32]:
# Подготовим данные для графика
sorted_data8 = sorted_data8[sorted_data8['street'].isin(top_15_str['street'])]
sorted_data8 = pd.merge(sorted_data8, top_15_str, on='street')
sorted_data8.rename(columns={'count_x': 'count_cat', 'count_y': 'count_str'}, inplace=True)
In [33]:
# Отсортируем данные
sorted_data8 = sorted_data8.sort_values(by=['count_str', 'count_cat'], ascending=False)

# Создание столбчатой диаграммы
fig8 = px.bar(sorted_data8, x='street', y='count_cat', color='category', labels={
    'category': 'Категория заведения',
    'street': 'Улица',
    'count_cat': 'Количество заведений'
})

# Настройка внешнего вида диаграммы
fig8.update_layout(title='ТОП-15 улиц по количеству заведений',
                  xaxis_title='Улица',
                  yaxis_title='Количество заведений')

fig8.show()

Выводы

  • больше всего заведений общепита расположено на проспекте Мира
  • во всех улицах кафе, рестораны и кофейни представляют значительную долю заведений общепита
  • выделяется МКАД: более половины всех заведений здесь работают в формате кафе, следующий по популярности - быстрое питание.

Найдем улицы, на которых находится только один объект общепита¶

In [34]:
# Выявляем улицы, где есть только 1 заведение
sorted_data9 = data.groupby(['street']).agg({'address': 'count'}).query('address==1').reset_index()
print('Всего улиц, на которых  находится только один объект общепита:', sorted_data9['street'].nunique())
print()
print(sorted_data9['street'].unique())
Всего улиц, на которых  находится только один объект общепита: 458

['1-й Автозаводский проезд' '1-й Балтийский переулок'
 '1-й Варшавский проезд' ... 'улица Шухова' 'улица Юннатов' '№ 7']

Посмотрим, какие заведения расположены на таких улицах.

In [35]:
# Фильтруем таблицу data, чтобы оставить только улицы, где есть только 1 заведение
sorted_data10 = pd.merge(sorted_data9, data, on='street')

print('Всего', sorted_data10['name'].nunique(), 'заведений являются единственными заведениями на улице.')
print('----')
print('Из них', sorted_data10.query('chain==1')['name'].count(), 'являются сетевыми заведениями.')
print('----')
print('Список заведения, которые являются единственными заведениями на улице:')
print()
print(sorted_data10['name'].unique())
Всего 403 заведений являются единственными заведениями на улице.
----
Из них 133 являются сетевыми заведениями.
----
Список заведения, которые являются единственными заведениями на улице:

['Чайхана Азия' 'Хуан Хэ' 'Колизей' ... 'Мираж' 'Scirocco' 'Енот']

Вывод

  • в основном единственными заведениями на улице являются менее узнаваемые заведения, которые не являются сетевыми.

Проанализируем примерную стоимость заказа в рублях и её изменение в зависимости от расположения завдения¶

Для каждого округа посчитаем медианное значение стоимости заказа в заведениях, которые находятся на его территории.

In [36]:
# Фильтруем только те строки, где у нас есть числовые значения для заказа
bill_data = data.query('middle_avg_bill != "N/A"')

# Переводим в числовой формат
bill_data['middle_avg_bill'] = pd.to_numeric(bill_data['middle_avg_bill'])

# Готовим данные для отражения на карте
bill_data = bill_data.groupby('district', as_index=False)['middle_avg_bill'].agg('median')\
            .sort_values(by='middle_avg_bill', ascending=False).reset_index().drop('index', axis=1)
bill_data
Out[36]:
district middle_avg_bill
0 Западный административный округ 1000.0
1 Центральный административный округ 1000.0
2 Северо-Западный административный округ 700.0
3 Северный административный округ 650.0
4 Юго-Западный административный округ 600.0
5 Восточный административный округ 575.0
6 Северо-Восточный административный округ 500.0
7 Южный административный округ 500.0
8 Юго-Восточный административный округ 450.0

Изобразим полученные значения на карте Москвы, построим фоновую картограмму.

In [37]:
# создаём карту Москвы
m2 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=bill_data,
    columns=['district', 'middle_avg_bill'],
    key_on='feature.name',
    fill_color='PuBu',
    fill_opacity=0.8,
    legend_name='Медианная стоимость заказа',
).add_to(m2)

# выводим карту
m2
Out[37]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Вывод

  • самое высокое значение среднего чека выявлено у ЦАО и ЗАО (где расположен деловой центр Москва-Сити)
  • при удалении от центра Москвы средний чек заведений снижается
  • на западе Москвы средний чек у заведений общепита выше, чем на востоке.

Общий вывод по разделу¶

  • В Москве представлено больше всего заведений общественного питания в формате кафе и ресторанов, меньше всего - столовых и булочных.
  • Усредненные рейтинги в разных типах общепита не сильно отличаются и находятся в пределах от 4.2 до 4.4.
  • В Москве 38% всех заведений являются сетевыми. Кафе и ресторан обычно не относятся к какой-либо сети (доля сетевых меньше 40%), а вот у кофеен, пиццерий и булочны сетевые и несетевые заведения представлены почти в одинаковом количестве.
  • Самыми крупными заведениями по количеству посадочных мест обычно являются рестораны, самыми маленькими - булочные.
  • Самой популярной сетью по количеству заведений является "Шоколадница", следующими по популярности являются сети пиццерий "Домино'c Пицца" и "Додо Пицца". Сети, которые вошли ТОП-15 по количеству заведений, обычно являются кофейнями, пиццериями или другие заведения с узнаваемым брендом. Эти заведения имеют своих постоянных клиентов и пользуются популярностью и в других регионах.
  • В 458 улицах Москвы расположено всего 1 заведение. В основном единственными заведениями на улице являются менее узнаваемые заведения, которые не являются сетевыми. Улицей, где расположено больше всего заведений общепита является проспект Мира.
  • Среди районов Москвы выделяетсы ЦАО: здесь представлено больше всего заведений общественного питания, также здесь самый высокий средний чек и самый высокий средний рейтинг заведений. В ЦАО самой распространенной категорией являются рестораны, а в остальных районах - кафе.
  • При удалении от центра Москвы средний чек заведений снижается, а на западе Москвы средний чек у заведений общепита выше, чем на востоке. В ЗАО средний чек сопоставим со средним чеком ЦАО. Это, скорее всего связано с тем, что ЗАО также является деловым центром (там расположен Москва-Сити).

Детализируем исследование: открытие кофейни¶

Основателям фонда «Shut Up and Take My Money» не даёт покоя успех сериала «Друзья». Их мечта — открыть такую же крутую и доступную, как «Central Perk», кофейню в Москве. Заказчики не боятся конкуренции в этой сфере, ведь кофеен в больших городах уже достаточно. Попробуем проанализировать, осуществима ли эта мечта.

Особенности расположения¶

Кофейню потенциально можно расположить в двух местах: 1) на улице, где отсутствуют другие кофейни, и конкуренция

2) на улице, где существует большая концентрация кофеен, и, соответственно,потребители специально туда едут за кофе из-за наличия большого выбора (при этом надо учитывать, чтобы на этой улице отсутствовали сетевые кофейни, так как их потребители будут лояльны бренду)

Для начала посмотрим, как количество кофеен меняется в зависимости от района.

In [38]:
# Подготовим данные для графика
sorted_data11 = sorted_data6.query('category=="кофейня"').sort_values(by='count_cat', ascending=False)

# Создание столбчатой диаграммы
fig11 = go.Figure()

# Добавление столбцов
fig11.add_trace(go.Bar(x=sorted_data11['distr_short'][:-1], y=sorted_data11['count_cat'][:-1],
                        marker=dict(color='rgba(0, 0, 255, 1)'), # Можете выбрать свой цвет для столбцов
                       hovertemplate='<br>'.join([
                            'Административный район: %{x}',
                            'Количество кофеен: %{y}'
                        ])
                       ))
fig11.add_trace(go.Bar(x=sorted_data11['distr_short'][-1:], y=sorted_data11['count_cat'][-1:],
                        marker=dict(color='rgba(255, 0, 0, 1.0)'), # Можете выбрать свой цвет для последнего столбца
                       hovertemplate='<br>'.join([
                            'Административный район: %{x}',
                            'Количество кофеен: %{y}'
                        ])
                        ))

# Настройка внешнего вида диаграммы
fig11.update_layout(
    title={
        'text': 'Количество кофеен в районах Москвы',
        'font': {'size': 20}
    },
    xaxis_title={
        'text': 'Административный район',
        'font': {'size': 16}
    },
    yaxis_title={
        'text': 'Количество кофеен',
        'font': {'size': 16}
    },
    font=dict(
        size=15
    ), showlegend=False)
                   

# Установка размера графика
fig11.update_layout(width=950, height=600)

fig11.show()

Вывод

  • меньше всего кофеен расположено в СЗАО, кофейню лучше открыть там.

Выделим улицы СЗАО, где отсутствуют другие кофейни в новый датафрейм data_streets_0¶

Выделим те улицы СЗАО, где отсутствуют другие кофейни, и конкуренция.

In [39]:
# группируем данные по улицам и категориям, считаем количество заведений
sorted_data12 = data.groupby(['street', 'category']).agg({'address': 'count'}).reset_index()

# фильтруем улицы, на которых нет категории "кофейня"
sorted_data12 = sorted_data12.groupby('street').filter(lambda x: 'кофейня' not in x['category'].values)['street'].unique()

# создаем таблицу с информацией об улицах СЗАО , где отсутствует кофейня
data_streets_0 = data[data['street'].isin(sorted_data12)].reset_index().drop('index', axis=1)
data_streets_0 = data_streets_0.query('distr_short=="СЗАО"').reset_index().drop('index', axis=1)
data_streets_0['street'].unique()
Out[39]:
array(['Парусный проезд', 'Походный проезд', 'Лодочная улица',
       'улица Мещерякова', 'бульвар Яна Райниса', 'улица Фомичёвой',
       'улица Фабрициуса', 'улица Василия Петушкова',
       'улица Вилиса Лациса', 'Строительный проезд', 'проезд Донелайтиса',
       'Новопоселковая улица', 'Таллинская улица', 'улица Твардовского',
       'Одинцовская улица', 'улица Исаковского',
       'памятник природы Серебряный бор',
       '4-я линия Хорошёвского Серебряного Бора', 'улица Маршала Рыбалко',
       'улица Максимова', 'улица Маршала Соколовского',
       'улица Маршала Тухачевского', 'улица Расплетина',
       'улица Академика Бочвара', 'Живописная улица',
       'Иваньковское шоссе', '1-я линия Хорошёвского Серебряного Бора',
       'Большой Волоколамский проезд', 'улица Паршина',
       'Новохорошёвский проезд', 'улица Маршала Новикова',
       'улица Генерала Глаголева', 'улица Водников', 'улица Рогова',
       'улица Демьяна Бедного', 'улица Саляма Адиля',
       '1-й Силикатный проезд', 'Причальный проезд',
       '2-й Силикатный проезд'], dtype=object)

Выделим улицы СЗАО с большой концентрацией несетевых кофеен (2 и более) в новый датафрейм data_streets_1¶

In [40]:
# фильтруем данные и группуируем по улице
sorted_data18 = data.query('category=="кофейня" and distr_short=="СЗАО" and chain==0')\
        .groupby(['street']).agg({'address': 'count'}).reset_index()

# оставляем названия улицы СЗАО с большой концентрацией несетевых кофеен (2 и более)
sorted_data18 = sorted_data18.query('address>=2').reset_index().drop('index', axis=1)['street'].unique()

# создаем новый датафрейм со всеми данными из data, где будут улицы СЗАО с большой концентрацией несетевых кофеен
data_streets_1 = data[data['street'].isin(sorted_data18)].reset_index().drop('index', axis=1)
data_streets_1 = data_streets_1.query('category=="кофейня" and chain==0').reset_index().drop('index', axis=1)

print('улицы СЗАО с большой концентрацией несетевых кофеен (2 и более):')
print()
print(data_streets_1['street'].unique())
улицы СЗАО с большой концентрацией несетевых кофеен (2 и более):

['Сходненская улица' 'улица Маршала Катукова' 'бульвар Генерала Карбышева'
 'Шелепихинская набережная']
In [41]:
# Подготовим данные для графика
sorted_data17 = data_streets_1.groupby(['street']).agg({'address': 'count'}).reset_index()

# Отсортируем данные
sorted_data17 = sorted_data17.sort_values(by=['address'], ascending=True)

# Создание столбчатой диаграммы
fig17 = px.bar(sorted_data17, y='street', x='address', labels={
    'street': 'Улица',
    'address': 'Количество кофеен'
})

# Настройка внешнего вида диаграммы
fig17.update_layout(title={
        'text': 'Улицы СЗАО с большой концентрацией несетевых кофеен',
        'font': {'size': 20}
    },
    xaxis_title={
        'text': 'Количество кофеен',
        'font': {'size': 16}
    },
    yaxis_title={
        'text': 'Улица',
        'font': {'size': 16}
    })

fig17.update_xaxes(tickfont=dict(size=14), dtick=1)
fig17.update_yaxes(tickfont=dict(size=14))

fig17.show()

Вывод

Мы выделили улицы СЗАО, где лучше расположить кофейню. Проанализируем другие критерии, чтобы сузить выборку улиц.

Время работы¶

Проанализируем, много ли в Москве круглосуточных кофеен.

In [42]:
# готовим данные для графика
sorted_data13 = data.query('category=="кофейня"')['is_24_7'].value_counts().sort_values(ascending=False).reset_index()
sorted_data13.columns = ['is_24_7', 'count']
sorted_data13
Out[42]:
is_24_7 count
0 False 1354
1 True 59
In [43]:
# готовим данные для графика - переименуем булевые значения
sorted_data13['is_24_7'] = sorted_data13['is_24_7'].replace([False, True], ['не круглосуточные', 'круглосуточные'])


# строим круговую диаграмму
fig13 = go.Figure(data=[go.Pie(labels=sorted_data13['is_24_7'], # указываем значения, которые появятся на метках сегментов
                             values=sorted_data13['count'], # указываем данные, которые отобразятся на графике
                             pull = [0.1, 0])]) # добавляем аргумент, который выделит тип-лидер на графике
fig13.update_layout(title='Соотношение круглосуточных и не круглосуточных кофеен', # указываем заголовок графика
                  width=830, # указываем размеры графика
                  height=500,
                  annotations=[dict(x=1.12, # вручную настраиваем аннотацию легенды
                                    y=1.05,
                                    text='Количество кофеен',
                                    showarrow=False)],
                   font=dict(size=13))
fig13.show() # выводим график

Вывод

  • в Москве доля круглосуточных кофеен крайне низкая - 4.18%; скорее всего, это означает, что при прочих равных держать круглосуточную кофейню не выгодно (круглосуточными лучше делать бары/пабы)

  • делать круглосуточные кафе имеет смысл только чтобы заработать конкурентное преимущество в местах с большой концентрацией кофеен.

Проверим data_streets_1 на наличие круглосуточных кофеен¶

In [44]:
data_streets_1.query('is_24_7==True')
Out[44]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats street is_24_7 distr_short

Вывод

  • в улицах СЗАО с большой концентрацией несетевых кофеен отсутствуют круглосуточные кофейни.

Рейтинги¶

Посмотрим, как рейтинг кофеен меняются в зависимости от района.

In [45]:
# Определяем порядок сортировки
sorted_data14 = data.query('category=="кофейня"').groupby('distr_short').agg({'rating': 'median'}).sort_values('rating', ascending=False).reset_index()

# Задаем размер графика
plt.figure(figsize=(8, 6))

# Создаем график
graph14 = sns.barplot(x='distr_short', y='rating', data=sorted_data14)

# Поворачиваем подписи на 45 градусов
labels14 = graph14.get_xticklabels()
graph14.set_xticklabels(labels14, rotation=45)

# Добавляем значения над каждым столбцом
for index, row in sorted_data14.iterrows():
    graph14.annotate('{:.1f}'.format(row['rating']), xy=(index, row['rating']), ha='center', va='bottom')

# ограничиваем ось Y для наглядности
plt.ylim(4, 4.4)
    
# Добавляем заголовок и подписи осей
plt.title('Медианные рейтинги кофеен')
plt.xlabel('Административные районы')
plt.ylabel('Медианный рейтинг')

# Отображаем график
plt.show()

Вывод

  • медианные рейтинги кофеен не отличаются между районами Москвы.

Проанализируем средние рейтинги в data_streets_1 (улицы с большой концентрацией несетевых кофеен)¶

In [46]:
# определяем порядок сортировки
sorted_data15 = data_streets_1.groupby('street').agg({'rating': 'median'}).sort_values('rating', ascending=False).reset_index()
sorted_data15['rating'] = sorted_data15['rating'].apply(lambda x: round(x, 2))

# строим столбчатую диаграмму 
fig15 = px.bar(sorted_data15.sort_values(by='rating', ascending=True), # загружаем данные и заново их сортируем
             x='rating', # указываем столбец с данными для оси X
             y='street', # указываем столбец с данными для оси Y
             text='rating', # добавляем аргумент, который отобразит текст с информацией о количестве
             hover_data=['rating'] # добавляем аргумент, который отобразит категорию при наведении курсора
            )

# оформляем график
fig15.update_layout(title='Рейтинги кофеен на улицах с большой концентрацией несетевых кофеен',
                   xaxis_title='Медианный рейтинг',
                   yaxis_title='Улица',
                   font=dict(size=13)) # увеличиваем размер шрифта

# красим нижний столбец в красный
fig15.update_traces(marker=dict(color=['red' if x == sorted_data15['rating'].max() else 'blue' for x in sorted_data15['rating']]))

fig15.update_xaxes(range=[3.6, 4.8])
fig15.show() # выводим график

Вывод

На улице Маршала Катукова медианный пользовательский рейтинг кофеен самый низкий среди ТОП-4 улиц СЗАО по концентрации несетевых кофеен. На этой улице есть возможность открыть кафе и навязать конкуренцию.

Проанализируем средние рейтинги в data_streets_0 (улицы, где отсутствуют кофейни)¶

Так как мы до этого выявили много улиц СЗАО, где отсутствуют кофейни, сразу поставим фильтр на средний рейтинг заведений меньше 4 и покажем оставшиеся улицы на графике.

In [47]:
# определяем порядок сортировки
sorted_data16 = data_streets_0.groupby('street').agg({'rating': 'median'}).query('rating<4').sort_values('rating', ascending=False).reset_index()

# ручная корректировка самого крупного значения, чтобы максимум был только один (нужно, чтобы удалось закрасить только один элемент)
sorted_data16.loc[0, 'rating'] = sorted_data16.loc[0, 'rating']+0.01
sorted_data16['rating'] = sorted_data16['rating'].apply(lambda x: round(x, 2))


# строим столбчатую диаграмму 
fig16 = px.bar(sorted_data16.sort_values(by='rating', ascending=True), # загружаем данные и заново их сортируем
             x='rating', # указываем столбец с данными для оси X
             y='street', # указываем столбец с данными для оси Y
             text='rating', # добавляем аргумент, который отобразит текст с информацией о количестве
             hover_data=['rating'] # добавляем аргумент, который отобразит категорию при наведении курсора
            )

# оформляем график
fig16.update_layout(title='Медианные рейтинги заведений на улицах без кофеен',
                   xaxis_title='Медианный рейтинг',
                   yaxis_title='Улица',
                    font=dict(size=13)) # увеличиваем размер шрифта

# красим нижний столбец в красный
fig16.update_traces(marker=dict(color=['red' if x == sorted_data16['rating'].max() else 'blue' for x in sorted_data16['rating']]))

fig16.update_xaxes(range=[2, 4])
fig16.show() # выводим график

Вывод

В Новохорошевском проезде отсутствуют кофейни, а заведения общепита, которые там есть, получили низкий рейтинг от посетителей. Следовательно, кофейню лучше открыть там.

Цены на капучино¶

Проанализируем медианную стоимость чашки капучино в Москве: для каждого округа посчитаем медианное значение стоимости капучино в кофейнях, которые находятся на его территории.

In [48]:
# Фильтруем только те строки, где у нас есть числовые значения для заказа
cof_data = data.query('middle_coffee_cup != "N/A" and category=="кофейня"')

# Переводим в числовой формат
cof_data['middle_coffee_cup'] = pd.to_numeric(cof_data['middle_coffee_cup'])

# Готовим данные для отражения на карте
cof_data = cof_data.groupby('district', as_index=False)['middle_coffee_cup'].agg('median')\
            .sort_values(by='middle_coffee_cup', ascending=False).reset_index().drop('index', axis=1)
cof_data
Out[48]:
district middle_coffee_cup
0 Юго-Западный административный округ 198.0
1 Центральный административный округ 190.0
2 Западный административный округ 189.0
3 Северо-Западный административный округ 165.0
4 Северо-Восточный административный округ 162.5
5 Северный административный округ 159.0
6 Южный административный округ 150.0
7 Юго-Восточный административный округ 147.5
8 Восточный административный округ 135.0

Изобразим полученные значения на карте Москвы, построим фоновую картограмму.

In [49]:
# создаём карту Москвы
m3 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=cof_data,
    columns=['district', 'middle_coffee_cup'],
    key_on='feature.name',
    fill_color='Oranges',
    fill_opacity=0.8,
    legend_name='Медианная стоимость капучино'
).add_to(m3)

# выводим карту
m3
Out[49]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Вывод

Цена на капучино заметно отличается в зависимости от района Москвы. Так как кофейня будет расположена в СЗАО, при открытии стоит ориентироваться на стоимость равную 165 руб.

Общий вывод по разделу¶

Рекомендуется открыть кофейню в СЗАО на Новохорошевском проезде или на улице Маршала Катукова.

  • На улице Маршала Чуйкова большая концентрация несетевых кофеен и, следовательно, большой поток посетителей. Кофейню можно сделать круглосуточным для достижения конкурентного преимущества.
  • На Новохорошевском проезде, наоборот, отсутствуют кофейни, а имеющиеся заведения общепита имеют низкий рейтинг у посетителей.

При открытии кофейни стоит ориентироваться на медианную стоимость капучино в СЗАО - 165 руб.

Презентация: https://disk.yandex.ru/i/dJzZAFEZeNBPHw